package beautiful.butterfly.server.httpserver.mvc.http;

import beautiful.butterfly.server.httpserver.exception.NotFoundException;
import beautiful.butterfly.server.httpserver.handler.FileChannelProgressiveFutureListener;
import beautiful.butterfly.server.httpserver.handler.HttpConst;
import beautiful.butterfly.server.httpserver.kit.StringKit;
import beautiful.butterfly.server.httpserver.mvc.Constant;
import beautiful.butterfly.server.httpserver.mvc.core.ActionContext;
import beautiful.butterfly.server.httpserver.mvc.render.Render;
import beautiful.butterfly.server.httpserver.mvc.wrapper.OutputStreamWrapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.file.Files;
import java.util.*;

import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;


@Slf4j
public class HttpResponse implements Response {

    private HttpHeaders httpHeaders = new DefaultHttpHeaders(false);
    private Set<Cookie> cookieSet = new HashSet<>(4);


    private ChannelHandlerContext channelHandlerContext = null;
    private CharSequence contentType = null;
    private CharSequence dateString = null;
    //
    private boolean isCommit = false;
    private int statusCode = 200;

    //
    public static HttpResponse build(ChannelHandlerContext channelHandlerContext, CharSequence dateString) {
        HttpResponse httpResponse = new HttpResponse();
        httpResponse.channelHandlerContext = channelHandlerContext;
        httpResponse.dateString = dateString;
        return httpResponse;
    }

    @Override
    public boolean isCommit() {
        return isCommit;
    }

    @Override
    public Response setStatus(int status) {
        this.statusCode = status;
        return this;
    }

    @Override
    public int getStatusCode() {
        return this.statusCode;
    }

    @Override
    public Response setContentType(@NonNull CharSequence contentType) {
        this.contentType = contentType;
        return this;
    }

    @Override
    public String getContentType() {
        return null == this.contentType ? null : String.valueOf(this.contentType);
    }

    @Override
    public Response setHeader(CharSequence name, CharSequence value) {
        this.httpHeaders.set(name, value);
        return this;
    }

    @Override
    public Map<String, String> headerMap() {
        Map<String, String> map = new HashMap<>(this.httpHeaders.size());
        for (Map.Entry<String, String> entry :
                this.httpHeaders.entries()) {
            map.put(entry.getKey(), entry.getValue());
        }
        return map;
    }


    @Override
    public Response cookie(@NonNull Cookies Cookies) {
        Cookie cookie = new io.netty.handler.codec.http.cookie.DefaultCookie(Cookies.name(), Cookies.value());
        if (Cookies.domain() != null) {
            cookie.setDomain(Cookies.domain());
        }
        if (Cookies.maxAge() > 0) {
            cookie.setMaxAge(Cookies.maxAge());
        }
        cookie.setPath(Cookies.path());
        cookie.setHttpOnly(Cookies.httpOnly());
        cookie.setSecure(Cookies.secure());
        this.cookieSet.add(cookie);
        return this;
    }

    @Override
    public Response cookie(String name, String value) {
        this.cookieSet.add(new io.netty.handler.codec.http.cookie.DefaultCookie(name, value));
        return this;
    }

    @Override
    public Response cookie(@NonNull String name, @NonNull String value, int maxAge) {
        Cookie nettyCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(name, value);
        nettyCookie.setPath("/");
        nettyCookie.setMaxAge(maxAge);
        this.cookieSet.add(nettyCookie);
        return this;
    }

    @Override
    public Response cookie(@NonNull String name, @NonNull String value, int maxAge, boolean secured) {
        Cookie nettyCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(name, value);
        nettyCookie.setPath("/");
        nettyCookie.setMaxAge(maxAge);
        nettyCookie.setSecure(secured);
        this.cookieSet.add(nettyCookie);
        return this;
    }

    @Override
    public Response cookie(@NonNull String path, @NonNull String name, @NonNull String value, int maxAge, boolean secured) {
        Cookie nettyCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(name, value);
        nettyCookie.setMaxAge(maxAge);
        nettyCookie.setSecure(secured);
        nettyCookie.setPath(path);
        this.cookieSet.add(nettyCookie);
        return this;
    }

    @Override
    public Response removeCookie(@NonNull String name) {

        Iterator<Cookie> iterator = this.cookieSet.iterator();
        while (iterator.hasNext()) {
            Cookie cookie = iterator.next();
            if (cookie.name().equals(name)) {
                cookie.setValue("");
                cookie.setMaxAge(-1);
                break;
            }
        }

        Cookie nettyCookie = new io.netty.handler.codec.http.cookie.DefaultCookie(name, "");
        nettyCookie.setMaxAge(-1);
        this.cookieSet.add(nettyCookie);
        return this;
    }

    @Override
    public Map<String, String> cookieMap() {
        Map<String, String> map = new HashMap<>(8);

        Iterator<Cookie> iterator = this.cookieSet.iterator();
        while (iterator.hasNext()) {
            Cookie cookie = iterator.next();
            map.put(cookie.name(), cookie.value());
        }
        return map;
    }

    @Override
    public void download(@NonNull String fileName, @NonNull File file) throws Exception {
        if (!file.exists() || !file.isFile()) {
            throw new NotFoundException("Not found file: " + file.getPath());
        }
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        Long length = randomAccessFile.length();
        this.contentType = StringKit.mimeType(file.getName());
        io.netty.handler.codec.http.HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        HttpHeaders httpHeaders = httpResponse.headers().add(getDefaultHeader());
        boolean keepAlive = ActionContext.getRequest().keepAlive();
        if (keepAlive) {
            httpResponse.headers().set(HttpConst.CONNECTION, KEEP_ALIVE);
        }
        httpHeaders.set(HttpConst.CONTENT_TYPE, this.contentType);
        httpHeaders.set("Content-Disposition", "attachment; filename=" + new String(fileName.getBytes("UTF-8"), "ISO8859_1"));
        httpHeaders.setInt(HttpConst.CONTENT_LENGTH, length.intValue());

        // Write the initial line and the setHeader.
        channelHandlerContext.write(httpResponse);
        ChannelFuture channelFuture = channelHandlerContext.write(new DefaultFileRegion(randomAccessFile.getChannel(), 0, length), channelHandlerContext.newProgressivePromise());
        // Write the end marker.
        ChannelFuture lastContentFuture = channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        channelFuture.addListener(FileChannelProgressiveFutureListener.build(randomAccessFile));
        // Decide whether to close the connection or not.
        if (!keepAlive) {
            lastContentFuture.addListener(ChannelFutureListener.CLOSE);
        }
        isCommit = true;
    }

    @Override
    public OutputStreamWrapper outputStream() throws IOException {
        File file = Files.createTempFile("$", ".temp").toFile();
        OutputStream outputStream = new FileOutputStream(file);
        return new OutputStreamWrapper(outputStream, file, channelHandlerContext);
    }


    /**
     * 核心方法
     */


    CharSequence getContentType(CharSequence contentType) {
        if (null == contentType) {
            contentType = HttpConst.CONTENT_TYPE_HTML;
        }
        if (HttpConst.contentTypes.containsKey(contentType)) {
            return HttpConst.contentTypes.get(contentType);
        }
        HttpConst.contentTypes.put(contentType, AsciiString.cached(String.valueOf(contentType)));
        return HttpConst.contentTypes.get(contentType);
    }


    @Override
    public void send(@NonNull FullHttpResponse fullHttpResponse) {
        fullHttpResponse.headers().set(getDefaultHeader());

        boolean keepAlive = false;

        if (!fullHttpResponse.headers().contains(HttpConst.CONTENT_LENGTH)) {
            fullHttpResponse.headers().set(HttpConst.CONTENT_LENGTH, String.valueOf(fullHttpResponse.content().readableBytes()));
        }
        if (!keepAlive) {
            channelHandlerContext.write(fullHttpResponse).addListener(ChannelFutureListener.CLOSE);
        } else {
            fullHttpResponse.headers().set(HttpConst.CONNECTION, KEEP_ALIVE);
            channelHandlerContext.write(fullHttpResponse, channelHandlerContext.voidPromise());
        }
        isCommit = true;
    }

    private HttpHeaders getDefaultHeader() {
        httpHeaders.set(HttpConst.DATE, HttpConst.DATE);
        httpHeaders.set(HttpConst.CONTENT_TYPE, getContentType(this.contentType));
        httpHeaders.set(HttpConst.X_POWER_BY, HttpConst.VERSION);
        if (!httpHeaders.contains(HttpConst.SERVER)) {
            httpHeaders.set(HttpConst.SERVER, HttpConst.VERSION);
        }
        if (this.cookieSet.size() > 0) {

            Iterator<Cookie> iterator = this.cookieSet.iterator();
            while (iterator.hasNext()) {
                Cookie cookie = iterator.next();
                httpHeaders.add(HttpConst.SET_COOKIE, io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookie));
            }
        }
        return httpHeaders;
    }

    public void render(@NonNull Render render) {
        try {
            ByteBuf byteBuf = Unpooled.wrappedBuffer(render.render().toString().getBytes("utf-8"));
            FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(statusCode), byteBuf);
            this.send(fullHttpResponse);
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
    }


    @Override
    public void redirect(@NonNull String url) {
        httpHeaders.set(HttpConst.LOCATION, url);
        FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
        this.send(fullHttpResponse);
    }

    public void html(String html) {
        if (null == html) return;
        FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(getStatusCode()), Unpooled.wrappedBuffer(html.getBytes(CharsetUtil.UTF_8)), false);
        if (null == this.getContentType())
            this.setContentType(Constant.CONTENT_TYPE_HTML);
        this.send(fullHttpResponse);
    }


    /**
     * Send getResponse body by ByteBuf.
     */
    public void body(ByteBuf byteBuf) {
        if (null == byteBuf) return;
        FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(getStatusCode()), byteBuf, false);
        this.send(fullHttpResponse);
    }


}